Skip to content

Commit 0b5cdaf

Browse files
fix: Inquiry SLA updates not being reflected in real time (#36815)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent df2f037 commit 0b5cdaf

File tree

8 files changed

+194
-9
lines changed

8 files changed

+194
-9
lines changed

.changeset/nice-experts-joke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@rocket.chat/meteor": patch
3+
---
4+
5+
Fixes queued conversations not being sorted in real time based on the room's SLA policy

apps/meteor/client/omnichannel/additionalForms/SlaPoliciesSelect.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { IOmnichannelServiceLevelAgreements, Serialized } from '@rocket.chat/core-typings';
22
import type { SelectOption } from '@rocket.chat/fuselage';
33
import { Field, FieldLabel, FieldRow, Select } from '@rocket.chat/fuselage';
4-
import { useMemo } from 'react';
4+
import { useId, useMemo } from 'react';
55

66
import { useHasLicenseModule } from '../../hooks/useHasLicenseModule';
77

@@ -15,16 +15,17 @@ type SlaPoliciesSelectProps = {
1515
export const SlaPoliciesSelect = ({ value, label, options, onChange }: SlaPoliciesSelectProps) => {
1616
const hasLicense = useHasLicenseModule('livechat-enterprise');
1717
const optionsSelect = useMemo<SelectOption[]>(() => options?.map((option) => [option._id, option.name]), [options]);
18+
const fieldId = useId();
1819

1920
if (!hasLicense) {
2021
return null;
2122
}
2223

2324
return (
2425
<Field>
25-
<FieldLabel>{label}</FieldLabel>
26+
<FieldLabel id={fieldId}>{label}</FieldLabel>
2627
<FieldRow>
27-
<Select value={value} options={optionsSelect} onChange={(value) => onChange(String(value))} />
28+
<Select aria-labelledby={fieldId} value={value} options={optionsSelect} onChange={(value) => onChange(String(value))} />
2829
</FieldRow>
2930
</Field>
3031
);

apps/meteor/client/views/omnichannel/directory/components/SlaField.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Box } from '@rocket.chat/fuselage';
2+
import { useId } from 'react';
23
import { useTranslation } from 'react-i18next';
34

45
import { FormSkeleton } from './FormSkeleton';
@@ -14,6 +15,7 @@ type SlaFieldProps = {
1415
const SlaField = ({ id }: SlaFieldProps) => {
1516
const { t } = useTranslation();
1617
const { data, isLoading, isError } = useSlaInfo(id);
18+
const slaFieldId = useId();
1719

1820
if (isLoading) {
1921
return <FormSkeleton />;
@@ -26,8 +28,8 @@ const SlaField = ({ id }: SlaFieldProps) => {
2628
const { name } = data;
2729
return (
2830
<Field>
29-
<Label>{t('SLA_Policy')}</Label>
30-
<Info>{name}</Info>
31+
<Label id={slaFieldId}>{t('SLA_Policy')}</Label>
32+
<Info aria-labelledby={slaFieldId}>{name}</Info>
3133
</Field>
3234
);
3335
};

apps/meteor/ee/app/livechat-enterprise/server/lib/SlaHelper.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { Message } from '@rocket.chat/core-services';
22
import type { IOmnichannelServiceLevelAgreements, IUser } from '@rocket.chat/core-typings';
33
import { LivechatInquiry, LivechatRooms } from '@rocket.chat/models';
44

5-
import { notifyOnRoomChangedById, notifyOnLivechatInquiryChangedByRoom } from '../../../../../app/lib/server/lib/notifyListener';
5+
import {
6+
notifyOnRoomChangedById,
7+
notifyOnLivechatInquiryChangedByRoom,
8+
notifyOnLivechatInquiryChanged,
9+
} from '../../../../../app/lib/server/lib/notifyListener';
610
import { callbacks } from '../../../../../lib/callbacks';
711

812
export const removeSLAFromRooms = async (slaId: string, userId: string) => {
@@ -19,7 +23,7 @@ export const removeSLAFromRooms = async (slaId: string, userId: string) => {
1923
};
2024

2125
export const updateInquiryQueueSla = async (roomId: string, sla: Pick<IOmnichannelServiceLevelAgreements, 'dueTimeInMinutes' | '_id'>) => {
22-
const inquiry = await LivechatInquiry.findOneByRoomId(roomId, { projection: { rid: 1, ts: 1 } });
26+
const inquiry = await LivechatInquiry.findOneByRoomId(roomId);
2327
if (!inquiry) {
2428
return;
2529
}
@@ -32,6 +36,8 @@ export const updateInquiryQueueSla = async (roomId: string, sla: Pick<IOmnichann
3236
slaId,
3337
estimatedWaitingTimeQueue,
3438
});
39+
40+
void notifyOnLivechatInquiryChanged({ ...inquiry, slaId, estimatedWaitingTimeQueue, _updatedAt: new Date() }, 'updated');
3541
};
3642

3743
export const updateRoomSlaWeights = async (roomId: string, sla: Pick<IOmnichannelServiceLevelAgreements, 'dueTimeInMinutes' | '_id'>) => {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import {
2+
OmnichannelSortingMechanismSettingType,
3+
type IOmnichannelServiceLevelAgreements,
4+
type Serialized,
5+
} from '@rocket.chat/core-typings';
6+
7+
import { createFakeVisitor } from '../../mocks/data';
8+
import { IS_EE } from '../config/constants';
9+
import { Users } from '../fixtures/userStates';
10+
import { HomeOmnichannel } from '../page-objects';
11+
import { OmnichannelRoomInfo } from '../page-objects/omnichannel-room-info';
12+
import { createConversation } from '../utils/omnichannel/rooms';
13+
import { createSLA } from '../utils/omnichannel/sla';
14+
import { test, expect } from '../utils/test';
15+
16+
const visitorA = createFakeVisitor();
17+
const visitorB = createFakeVisitor();
18+
const visitorC = createFakeVisitor();
19+
20+
test.skip(!IS_EE, 'Omnichannel SLAs > Enterprise Only');
21+
22+
test.use({ storageState: Users.user1.state });
23+
24+
test.describe('OC - SLA Policies [Sidebar]', () => {
25+
let poHomeChannel: HomeOmnichannel;
26+
let poRoomInfo: OmnichannelRoomInfo;
27+
let conversations: Awaited<ReturnType<typeof createConversation>>[] = [];
28+
let slas: Serialized<Omit<IOmnichannelServiceLevelAgreements, '_updatedAt'>>[] = [];
29+
30+
test.beforeAll('create SLAs', async ({ api }) => {
31+
slas = await Promise.all([
32+
createSLA(api, { name: 'Very Urgent', dueTimeInMinutes: 1 }),
33+
createSLA(api, { name: 'Urgent', dueTimeInMinutes: 10 }),
34+
createSLA(api, { name: 'Not Urgent', dueTimeInMinutes: 30 }),
35+
]);
36+
});
37+
38+
test.beforeAll(async ({ api }) => {
39+
(
40+
await Promise.all([
41+
// Create agent and manager
42+
api.post('/livechat/users/agent', { username: 'user1' }),
43+
api.post('/livechat/users/manager', { username: 'user1' }),
44+
// Settings
45+
api.post('/settings/Livechat_Routing_Method', { value: 'Manual_Selection' }),
46+
api.post('/settings/Omnichannel_sorting_mechanism', { value: OmnichannelSortingMechanismSettingType.SLAs }),
47+
])
48+
).every((res) => expect(res.status()).toBe(200));
49+
});
50+
51+
test.beforeEach(async ({ page }) => {
52+
poHomeChannel = new HomeOmnichannel(page);
53+
poRoomInfo = new OmnichannelRoomInfo(page);
54+
});
55+
56+
test.beforeEach(async ({ page }) => {
57+
await page.goto('/');
58+
await page.locator('#main-content').waitFor();
59+
});
60+
61+
test.beforeEach(async ({ api }) => {
62+
conversations = await Promise.all([
63+
createConversation(api, { visitorName: visitorA.name, agentId: 'user1' }),
64+
createConversation(api, { visitorName: visitorB.name, agentId: 'user1' }),
65+
createConversation(api, { visitorName: visitorC.name, agentId: 'user1' }),
66+
]);
67+
});
68+
69+
test.afterAll('delete SLAs', async ({ api }) => {
70+
const responses = await Promise.all(slas.map((sla) => api.delete(`/livechat/sla/${sla._id}`)));
71+
responses.every((res) => expect(res.status()).toBe(200));
72+
});
73+
74+
test.afterAll(async ({ api }) => {
75+
// Delete conversations
76+
await Promise.all(conversations.map((conversation) => conversation.delete()));
77+
78+
(
79+
await Promise.all([
80+
// Delete agent and manager
81+
api.delete('/livechat/users/agent/user1'),
82+
api.delete('/livechat/users/manager/user1'),
83+
// Reset settings
84+
api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' }),
85+
api.post('/settings/Omnichannel_sorting_mechanism', { value: OmnichannelSortingMechanismSettingType.Timestamp }),
86+
])
87+
).every((res) => expect(res.status()).toBe(200));
88+
});
89+
90+
test('OC - SLA Policies [Sidebar] - Update conversation SLA Policy', async () => {
91+
await test.step('expect to change room SLA policy to "Not urgent"', async () => {
92+
await test.step('expect to open room and room info to be visible', async () => {
93+
await poHomeChannel.sidenav.getSidebarItemByName(visitorA.name).click();
94+
await expect(poRoomInfo.dialogRoomInfo).toBeVisible();
95+
});
96+
97+
await test.step('expect to update room SLA policy', async () => {
98+
await expect(poRoomInfo.getInfoByLabel('SLA Policy')).not.toBeVisible();
99+
await poRoomInfo.btnEditRoomInfo.click();
100+
await poRoomInfo.selectSLA('Not Urgent');
101+
await poRoomInfo.btnSaveEditRoom.click();
102+
});
103+
104+
await test.step('expect SLA to have been updated in the room info and queue order to be correct', async () => {
105+
await expect(poRoomInfo.getInfoByLabel('SLA Policy')).toHaveText('Not Urgent');
106+
await expect(poHomeChannel.sidenav.getSidebarListItemByName(visitorA.name)).toHaveAttribute('data-index', '1');
107+
});
108+
});
109+
110+
await test.step('expect to change room SLA policy to "Urgent"', async () => {
111+
await test.step('expect to open room and room info to be visible', async () => {
112+
await poHomeChannel.sidenav.getSidebarItemByName(visitorB.name).click();
113+
await expect(poRoomInfo.dialogRoomInfo).toBeVisible();
114+
});
115+
116+
await test.step('expect to update room SLA policy', async () => {
117+
await expect(poRoomInfo.getInfoByLabel('SLA Policy')).not.toBeVisible();
118+
await poRoomInfo.btnEditRoomInfo.click();
119+
await poRoomInfo.selectSLA('Urgent');
120+
await poRoomInfo.btnSaveEditRoom.click();
121+
});
122+
123+
await test.step('expect SLA to have been updated in the room info and queue order to be correct', async () => {
124+
await expect(poRoomInfo.getInfoByLabel('SLA Policy')).toHaveText('Urgent');
125+
await expect(poHomeChannel.sidenav.getSidebarListItemByName(visitorB.name)).toHaveAttribute('data-index', '1');
126+
await expect(poHomeChannel.sidenav.getSidebarListItemByName(visitorA.name)).toHaveAttribute('data-index', '2');
127+
});
128+
});
129+
130+
await test.step('expect to change room SLA policy to "Very Urgent"', async () => {
131+
await test.step('expect to open room and room info to be visible', async () => {
132+
await poHomeChannel.sidenav.getSidebarItemByName(visitorC.name).click();
133+
await expect(poRoomInfo.dialogRoomInfo).toBeVisible();
134+
});
135+
136+
await test.step('expect to update room SLA policy', async () => {
137+
await expect(poRoomInfo.getInfoByLabel('SLA Policy')).not.toBeVisible();
138+
await poRoomInfo.btnEditRoomInfo.click();
139+
await poRoomInfo.selectSLA('Very Urgent');
140+
await poRoomInfo.btnSaveEditRoom.click();
141+
});
142+
143+
await test.step('expect SLA to have been updated in the room info and queue order to be correct', async () => {
144+
await expect(poRoomInfo.getInfoByLabel('SLA Policy')).toHaveText('Very Urgent');
145+
await expect(poHomeChannel.sidenav.getSidebarListItemByName(visitorC.name)).toHaveAttribute('data-index', '1');
146+
await expect(poHomeChannel.sidenav.getSidebarListItemByName(visitorB.name)).toHaveAttribute('data-index', '2');
147+
await expect(poHomeChannel.sidenav.getSidebarListItemByName(visitorA.name)).toHaveAttribute('data-index', '3');
148+
});
149+
});
150+
});
151+
});

apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ export class HomeSidenav {
109109
return this.page.getByRole('link').filter({ has: this.page.getByText(name, { exact: true }) });
110110
}
111111

112+
getSidebarListItemByName(name: string): Locator {
113+
return this.sidebarChannelsList.getByRole('listitem').filter({ has: this.getSidebarItemByName(name) });
114+
}
115+
112116
getSearchItemByName(name: string): Locator {
113117
return this.searchList.getByRole('link').filter({ has: this.page.getByText(name, { exact: true }) });
114118
}

apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ export class OmnichannelRoomInfo {
4040
return this.page.locator(`div >> text="${label}"`);
4141
}
4242

43+
getInfoByLabel(label: string): Locator {
44+
return this.dialogRoomInfo.getByLabel(label);
45+
}
46+
47+
get inputSLAPolicy(): Locator {
48+
return this.dialogEditRoom.getByRole('button', { name: 'SLA Policy' });
49+
}
50+
51+
async selectSLA(name: string): Promise<void> {
52+
await this.inputSLAPolicy.click();
53+
return this.page.getByRole('option', { name, exact: true }).click();
54+
}
55+
4356
getBadgeIndicator(name: string, title: string): Locator {
4457
return this.homeSidenav.getSidebarItemByName(name).getByTitle(title);
4558
}

apps/meteor/tests/e2e/utils/omnichannel/sla.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ export const generateRandomSLAData = (): Omit<IOmnichannelServiceLevelAgreements
1010
dueTimeInMinutes: faker.number.int({ min: 10, max: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE }),
1111
});
1212

13-
export const createSLA = async (api: BaseTest['api']): Promise<Omit<IOmnichannelServiceLevelAgreements, '_updated'>> => {
14-
const response = await api.post('/livechat/sla', generateRandomSLAData());
13+
export const createSLA = async (
14+
api: BaseTest['api'],
15+
slaData?: Omit<IOmnichannelServiceLevelAgreements, '_updatedAt' | '_id'>,
16+
): Promise<Omit<IOmnichannelServiceLevelAgreements, '_updated'>> => {
17+
const response = await api.post('/livechat/sla', slaData ?? generateRandomSLAData());
1518
expect(response.status()).toBe(200);
1619

1720
const { sla } = (await response.json()) as { sla: Omit<IOmnichannelServiceLevelAgreements, '_updated'> };

0 commit comments

Comments
 (0)